# -*- coding: UTF-8 -*-
from django.core import validators
from django.core.exceptions import PermissionDenied
from django.utils.html import escape
from django.utils.text import fix_microsoft_characters

FORM_FIELD_ID_PREFIX = 'id_'

class EmptyValue(Exception):
    "Cette exception est déclenchée lorsque des données vides sont spécifiées"
    pass

class Manipulator:
    # Une liste de permissions. L'utilisateur doit en posséder au moins une de la
    # liste pour pouvoir utiliser le manipulateur. La valeur None signifie que tout
    # le monde a la permission d'utiliser le manipulateur.
    required_permission = ''

    def __init__(self):
        # Une liste d'objets FormField
        self.fields = []

    def __getitem__(self, field_name):
        "Recherche le champ de formulaire dont le nom de champ est spécifié; déclenche une exception KeyError en cas d'échec."
        for field in self.fields:
            if field.field_name == field_name:
                return field
        raise KeyError, "Le champ %s est introuvable." % field_name

    def __delitem__(self, field_name):
        "Supprime le champ de formulaire dont le nom de champ est spécifié; déclenche une exception KeyError en cas d'échec."
        for i, field in enumerate(self.fields):
            if field.field_name == field_name:
                del self.fields[i]
                return
        raise KeyError, "Le champ %s est introuvable." % field_name

    def check_permissions(self, user):
        """Confirme si l'utilisateur a les permissions requises pour utiliser ce
        manipulateur; déclenche une exception PermissionDenied en cas d'échec."""
        if self.required_permission is None:
            return
        if user.has_perm(self.required_permission):
            return
        raise PermissionDenied

    def prepare(self, new_data):
        """
        Effectue toutes les préparations nécessaires sur l'argument new_data, qui est
        modifié directement par cette fonction; ces préparations doivent être réalisées
        avant la validation des données.
        """
        for field in self.fields:
            field.prepare(new_data)

    def get_validation_errors(self, new_data):
        "Renvoie un dictionnaire associant les noms de champ des champs du manipulateur aux listes de messages d'erreur correspondantes"
        errors = {}
        for field in self.fields:
            if field.is_required and not new_data.get(field.field_name, False):
                errors.setdefault(field.field_name, []).append('Ce champ est requis.')
                continue
            try:
                validator_list = field.validator_list
                if hasattr(self, 'validate_%s' % field.field_name):
                    validator_list.append(getattr(self, 'validate_%s' % field.field_name))
                for validator in validator_list:
                    if field.is_required or new_data.get(field.field_name, False) or hasattr(validator, 'always_test'):
                        try:
                            if hasattr(field, 'requires_data_list'):
                                validator(new_data.getlist(field.field_name), new_data)
                            else:
                                validator(new_data.get(field.field_name, ''), new_data)
                        except validators.ValidationError, e:
                            errors.setdefault(field.field_name, []).extend(e.messages)
            # Si une exception CriticalValidationError est déclenchée lors de la validation d'un champ,
            # on ignore toute autre exception ValidationError que pourrait déclencher ce champ particulier
            except validators.CriticalValidationError, e:
                errors.setdefault(field.field_name, []).extend(e.messages)
        return errors

    def save(self, new_data):
        "Enregistre les changements et renvoie le nouvel objet"
        # changes est un objet du style dictionnaire dont les clés sont des noms de champ
        raise NotImplementedError

    def do_html2python(self, new_data):
        """
        Convertit les données spécifiées pour qu'elles emploient des types de
        données Python et non HTML; l'argument new_data est modifié
        directement par cette fonction. Cette conversion doit s'effectuer après
        la validation mais avant le stockage. Elle doit s'effectuer après validation
        car les fonctions html2python s'attendent à manipuler des données valides.
        """
        for field in self.fields:
            if new_data.has_key(field.field_name):
                new_data.setlist(field.field_name,
                    [field.__class__.html2python(data) for data in new_data.getlist(field.field_name)])
            else:
                try:
                    # chaque champ doit individuellement traiter le cas de la valeur None
                    new_data.setlist(field.field_name, [field.__class__.html2python(None)])
                except EmptyValue:
                    new_data.setlist(field.field_name, [])

class FormWrapper:
    """
    Une classe construite autour de la classe Manipulator et qui est utile
    pour utiliser un manipulateur au sein d'un modèle. Avec cette classe, vous
    pouvez rechercher par leur nom les champs de formulaire (comme avec un dictionnaire).
    Cette classe permet aussi de remplir à l'avance les données associées au manipulateur
    ainsi que de spécifier (toujours à l'avance) des messages d'erreur de validation.
    """
    def __init__(self, manipulator, data, error_dict):
        self.manipulator, self.data = manipulator, data
        self.error_dict = error_dict

    def __repr__(self):
        return repr(self.data)

    def __getitem__(self, key):
        for field in self.manipulator.fields:
            if field.field_name == key:
                if hasattr(field, 'requires_data_list') and hasattr(self.data, 'getlist'):
                    data = self.data.getlist(field.field_name)
                else:
                    data = self.data.get(field.field_name, None)
                if data is None:
                    data = ''
                return FormFieldWrapper(field, data, self.error_dict.get(field.field_name, []))
        raise KeyError

    def has_errors(self):
        return self.error_dict != {}

class FormFieldWrapper:
    "Une passerelle entre vos modèles et un champ de formulaire particulier. Cette classe sert à la classe FormWrapper."
    def __init__(self, formfield, data, error_list):
        self.formfield, self.data, self.error_list = formfield, data, error_list
        self.field_name = self.formfield.field_name # plus pratique pour les modèles

    def __str__(self):
        "Génère le rendu du champ"
        return str(self.formfield.render(self.data))

    def __repr__(self):
        return '<Objet FormFieldWrapper associé au champ "%s">' % self.formfield.field_name

    def field_list(self):
        """
        Comme __str__() mais renvoie une liste. Utilisez cette méthode lorsque
        la méthode render() du champ renvoie une liste.
        """
        return self.formfield.render(self.data)

    def errors(self):
        return self.error_list

    def html_error_list(self):
        if self.errors():
            return '<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()])
        else:
            return ''

class FormFieldCollection(FormFieldWrapper):
    "Une classe pratique qui met à disposition de vos modèles un dictionnaire d'objets FormFieldWrapper"
    def __init__(self, formfield_dict):
        self.formfield_dict = formfield_dict

    def __str__(self):
        return str(self.formfield_dict)

    def __getitem__(self, template_key):
        "Recherche le champ spécifié; déclenche une exception KeyError en cas d'échec."
        return self.formfield_dict[template_key]

    def __repr__(self):
        return "<Objet FormFieldCollection : %s>" % self.formfield_dict

    def errors(self):
        "Renvoie une liste de toutes les erreurs associées aux champs de cette collection."
        errors = []
        for field in self.formfield_dict.values():
            errors.extend(field.errors())
        return errors

class FormField:
    """Classe abstraite représentant un champ de formulaire.

    Les classes qui étendent FormField définissent généralement les attributs suivants :
        field_name
            Le nom du champ, qui l'identifie auprès du reste du programme.
        validator_list
            Une liste de tests de validation (des fonctions en fait) que les
            données associées au champ doivent réussir pour être ajoutées ou
            modifiées.
        is_required
            Un booléen. Est-ce que le champ est un champ requis?
    Les sous-classes de cette classe implémentent aussi généralement une méthode
    render(data), qui est chargée de transformer le champ de formulaire en code
    XHTML.
    """
    def __str__(self):
        return self.render('')

    def __repr__(self):
        return 'Objet FormField "%s"' % self.field_name

    def prepare(self, new_data):
        "Un hook chargé de modifier (directement) new_data avant validation."
        pass

    def html2python(data):
        "Un hook chargé de convertir un type de données HTML (par exemple, la valeur 'on' des cases à cocher) en un type de données Python."
        return data
    html2python = staticmethod(html2python)

    def render(self, data):
        raise NotImplementedError

######################
# WIDGETS GÉNÉRIQUES #
######################

class TextField(FormField):
    def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[]):
        self.field_name = field_name
        self.length, self.maxlength = length, maxlength
        self.is_required = is_required
        self.validator_list = [self.isValidLength, self.hasNoNewlines] + validator_list

    def isValidLength(self, data, form):
        if data and self.maxlength and len(data) > self.maxlength:
            raise validators.ValidationError, "Assurez-vous que le texte entré ne dépasse pas %s caractères." % self.maxlength

    def hasNoNewlines(self, data, form):
        if data and '\n' in data:
            raise validators.ValidationError, "Les sauts de ligne sont ici interdits."

    def render(self, data):
        if data is None:
            data = ''
        maxlength = ''
        if self.maxlength:
            maxlength = 'maxlength="%s" ' % self.maxlength
        if isinstance(data, unicode):
            data = data.encode('utf-8')
        return '<input type="text" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
            self.field_name, self.length, escape(data), maxlength)

    def html2python(data):
        if data:
            return fix_microsoft_characters(data)
        return data
    html2python = staticmethod(html2python)

class PasswordField(TextField):
    def render(self, data):
        # l'attribut value est toujours vierge car nous ne voulons jamais ré-afficher ce widget
        return '<input type="password" id="%s" class="v%s%s" name="%s" value="" />' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
            self.field_name)

class LargeTextField(TextField):
    def __init__(self, field_name, rows=10, cols=40, is_required=False, validator_list=[], maxlength=None):
        self.field_name = field_name
        self.rows, self.cols, self.is_required = rows, cols, is_required
        self.validator_list = validator_list[:]
        if maxlength:
            self.validator_list.append(self.isValidLength)
            self.maxlength = maxlength

    def render(self, data):
        if data is None:
            data = ''
        if isinstance(data, unicode):
            data = data.encode('utf-8')
        return '<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
            self.field_name, self.rows, self.cols, escape(data))

class HiddenField(FormField):
    def __init__(self, field_name, is_required=False, validator_list=[]):
        self.field_name, self.is_required = field_name, is_required
        self.validator_list = validator_list[:]

    def render(self, data):
        return '<input type="hidden" id="%s" name="%s" value="%s" />' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.field_name, escape(data))

class CheckboxField(FormField):
    def __init__(self, field_name, checked_by_default=False):
        self.field_name = field_name
        self.checked_by_default = checked_by_default
        self.is_required, self.validator_list = False, [] # on définit ces attributs car les validateurs en ont besoin

    def render(self, data):
        checked_html = ''
        if data or (data is '' and self.checked_by_default):
            checked_html = ' checked="checked"'
        return '<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__,
            self.field_name, checked_html)

    def html2python(data):
        "Convertit la valeur provenant du navigateur ('on' ou '') en un booléen Python"
        if data == 'on':
            return True
        return False
    html2python = staticmethod(html2python)

class SelectField(FormField):
    def __init__(self, field_name, choices=[], size=1, is_required=False, validator_list=[]):
        self.field_name = field_name
        # choices doit être une liste de tuples de la forme (valeur, clé_descriptive), car l'ordre est important
        self.choices, self.size, self.is_required = choices, size, is_required
        self.validator_list = [self.isValidChoice] + validator_list

    def render(self, data):
        output = ['<select id="%s" class="v%s%s" name="%s" size="%s">' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
            self.field_name, self.size)]
        str_data = str(data) # on normalise les données en les plaçant dans une chaîne
        for value, display_name in self.choices:
            selected_html = ''
            if str(value) == str_data:
                selected_html = ' selected="selected"'
            output.append('    <option value="%s"%s>%s</option>' % (escape(value), selected_html, display_name))
        output.append('  </select>')
        return '\n'.join(output)

    def isValidChoice(self, data, form):
        str_data = str(data)
        str_choices = [str(item[0]) for item in self.choices]
        if str_data not in str_choices:
            raise validators.ValidationError, "Sélectionnez une option valide; '%s' ne fait pas partie de la liste d'options valides %s." % (str_data, str_choices)

class NullSelectField(SelectField):
    "Cette sous-classe de SelectField convertit une valeur vierge en l'objet None"
    def html2python(data):
        if not data:
            return None
        return data
    html2python = staticmethod(html2python)

class RadioSelectField(FormField):
    def __init__(self, field_name, choices=[], ul_class='', is_required=False, validator_list=[]):
        self.field_name = field_name
        # choices doit être une liste de tuples de la forme (valeur, clé_descriptive), car l'ordre est important
        self.choices, self.is_required = choices, is_required
        self.validator_list = [self.isValidChoice] + validator_list
        self.ul_class = ul_class

    def render(self, data):
        """
        Renvoie un objet spécial, RadioFieldRenderer, qui est itérable *mais*
        qui possède aussi un rendu par défaut, généré lorsque vous appelez str()
        sur un champ RadioSelectField.

        Cela rend plus souple l'usage d'un champ RadioSelectField dans vos modèles.
        Vous pouvez utiliser simplement le rendu par défaut :

            {{ nom_champ }}

        ...ce qui aura pour effet d'afficher les boutons radio au sein d'une liste
        non-ordonnée.
        Ou vous pouvez parcourir à la main chaque option du champ pour créer une mise
        en page plus personnalisée :

            {% for option in nom_champ.field_list %}
                {{ option.field }} {{ option.label }}<br />
            {% endfor %}
        """
        class RadioFieldRenderer:
            def __init__(self, datalist, ul_class):
                self.datalist, self.ul_class = datalist, ul_class
            def __str__(self):
                "La sortie par défaut renvoyée par str() lorsqu'on l'applique à ce champ -- une liste non-ordonnée (<ul>)"
                output = ['<ul%s>' % (self.ul_class and ' class="%s"' % self.ul_class or '')]
                output.extend(['<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist])
                output.append('</ul>')
                return ''.join(output)
            def __iter__(self):
                for d in self.datalist:
                    yield d
            def __len__(self):
                return len(self.datalist)
        datalist = []
        str_data = str(data) # on normalise les données en les plaçant dans une chaîne
        for i, (value, display_name) in enumerate(self.choices):
            selected_html = ''
            if str(value) == str_data:
                selected_html = ' checked="checked"'
            datalist.append({
                'value': value,
                'name': display_name,
                'field': '<input type="radio" id="%s" name="%s" value="%s"%s/>' % \
                    (FORM_FIELD_ID_PREFIX + self.field_name + '_' + str(i), self.field_name, value, selected_html),
                'label': '<label for="%s">%s</label>' % \
                    (FORM_FIELD_ID_PREFIX + self.field_name + '_' + str(i), display_name),
            })
        return RadioFieldRenderer(datalist, self.ul_class)

    def isValidChoice(self, data, form):
        str_data = str(data)
        str_choices = [str(item[0]) for item in self.choices]
        if str_data not in str_choices:
            raise validators.ValidationError, "Sélectionnez une option valide; '%s' ne fait pas partie de la liste d'options valides %s." % (str_data, str_choices)

class NullBooleanField(SelectField):
    "Cette sous-classe de SelectField offre trois options : 'Oui', 'Non' et 'Inconnu'; le résultat Python associé est True, False ou None (respectivement)"
    def __init__(self, field_name, is_required=False, validator_list=[]):
        SelectField.__init__(self, field_name, choices=[('1', 'Inconnu'), ('2', 'Oui'), ('3', 'Non')],
            is_required=is_required, validator_list=validator_list)

    def render(self, data):
        if data is None: data = '1'
        elif data == True: data = '2'
        elif data == False: data = '3'
        return SelectField.render(self, data)

    def html2python(data):
        return {'1': None, '2': True, '3': False}[data]
    html2python = staticmethod(html2python)

class SelectMultipleField(SelectField):
    requires_data_list = True
    def render(self, data):
        output = ['<select id="%s" class="v%s%s" name="%s" size="%s" multiple="multiple">' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, self.is_required and ' required' or '',
            self.field_name, self.size)]
        str_data_list = map(str, data) # on normalise les données afin de travailler avec une liste de chaînes
        for value, choice in self.choices:
            selected_html = ''
            if str(value) in str_data_list:
                selected_html = ' selected="selected"'
            output.append('    <option value="%s"%s>%s</option>' % (escape(value), selected_html, choice))
        output.append('  </select>')
        return '\n'.join(output)

    def isValidChoice(self, field_data, all_data):
        # les données ressemblent à quelque chose du style ['1', '2', '3']
        str_choices = [str(item[0]) for item in self.choices]
        for val in map(str, field_data):
            if val not in str_choices:
                raise validators.ValidationError, "Sélectionnez une option valide; '%s' ne fait pas partie de la liste d'options valides %s." % (val, str_choices)

    def html2python(data):
        if data is None:
            raise EmptyValue
        return data
    html2python = staticmethod(html2python)

class CheckboxSelectMultipleField(SelectMultipleField):
    """
    Ce champ possède une interface identique à celle du champ SelectMultipleField,
    sauf que son rendu est différent. Au lieu d'un élément <select multiple>, le
    widget généré est une liste non-ordonnée (<ul>) de cases à cocher
    (<input type="checkbox").

    Bien sûr, un tel rendu génère plusieurs éléments différents pour ce qui
    constitue en fait un seul et même champ de formulaire, et donc la méthode
    prepare() de cette classe se charge de rassembler les données éparses au
    sein d'une seule et même liste, que les validateurs, générateurs de rendu
    et autres méthodes save() peuvent manipuler comme ils veulent.
    """
    requires_data_list = True
    def __init__(self, field_name, choices=[], validator_list=[]):
        SelectMultipleField.__init__(self, field_name, choices, size=1, is_required=False, validator_list=validator_list)

    def prepare(self, new_data):
        # À l'intérieur de new_data, les données associées au champ sont réparties
        # sur plusieurs champs différents; cette méthode se charge donc de rassembler
        # ces valeurs éparses en une seule et même liste.
        data_list = []
        for value, _ in self.choices:
            if new_data.get('%s%s' % (self.field_name, value), '') == 'on':
                data_list.append(value)
        new_data.setlist(self.field_name, data_list)

    def render(self, data):
        output = ['<ul>']
        str_data_list = map(str, data) # on normalise les données afin de travailler avec une liste de chaînes
        for value, choice in self.choices:
            checked_html = ''
            if str(value) in str_data_list:
                checked_html = ' checked="checked"'
            field_name = '%s%s' % (self.field_name, value)
            output.append('<li><input type="checkbox" id="%s%s" class="v%s" name="%s"%s /> <label for="%s%s">%s</label></li>' % \
                (FORM_FIELD_ID_PREFIX, field_name, self.__class__.__name__, field_name, checked_html,
                FORM_FIELD_ID_PREFIX, field_name, choice))
        output.append('</ul>')
        return '\n'.join(output)

##########
# UPLOAD #
##########

class FileUploadField(FormField):
    def __init__(self, field_name, is_required=False, validator_list=[]):
        self.field_name, self.is_required = field_name, is_required
        self.validator_list = [self.isNonEmptyFile] + validator_list

    def isNonEmptyFile(self, field_data, all_data):
        if not field_data['content']:
            raise validators.CriticalValidationError, "Le fichier envoyé est vide."

    def render(self, data):
        return '<input type="file" id="%s" class="v%s" name="%s" />' % \
            (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__,
            self.field_name)

    def html2python(data):
        if data is None:
            raise EmptyValue
        return data
    html2python = staticmethod(html2python)

class ImageUploadField(FileUploadField):
    "Une sous-classe de FileUploadField qui déclenche une exception CriticalValidationError si le fichier uploadé n'est pas une image."
    def __init__(self, *args, **kwargs):
        FileUploadField.__init__(self, *args, **kwargs)
        self.validator_list.insert(0, self.isValidImage)

    def isValidImage(self, field_data, all_data):
        try:
            validators.isValidImage(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

#######################################
# ENTIERS/NOMBRES À VIRGULE FLOTTANTE #
#######################################

class IntegerField(TextField):
    def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]):
        validator_list = [self.isInteger] + validator_list
        TextField.__init__(self, field_name, length, maxlength, is_required, validator_list)

    def isInteger(self, field_data, all_data):
        try:
            validators.isInteger(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

    def html2python(data):
        if data == '' or data is None:
            return None
        return int(data)
    html2python = staticmethod(html2python)

class SmallIntegerField(IntegerField):
    def __init__(self, field_name, length=5, maxlength=5, is_required=False, validator_list=[]):
        validator_list = [self.isSmallInteger] + validator_list
        IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)

    def isSmallInteger(self, field_data, all_data):
        if not -32768 <= int(field_data) <= 32767:
            raise validators.CriticalValidationError, "Entrez un nombre entier entre -32 768 et 32 767."

class PositiveIntegerField(IntegerField):
    def __init__(self, field_name, length=10, maxlength=None, is_required=False, validator_list=[]):
        validator_list = [self.isPositive] + validator_list
        IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)

    def isPositive(self, field_data, all_data):
        if int(field_data) < 0:
            raise validators.CriticalValidationError, "Entrez un nombre positif."

class PositiveSmallIntegerField(IntegerField):
    def __init__(self, field_name, length=5, maxlength=None, is_required=False, validator_list=[]):
        validator_list = [self.isPositiveSmall] + validator_list
        IntegerField.__init__(self, field_name, length, maxlength, is_required, validator_list)

    def isPositiveSmall(self, field_data, all_data):
        if not 0 <= int(field_data) <= 32767:
            raise validators.CriticalValidationError, "Entrez un nombre entier entre 0 et 32 767."

class FloatField(TextField):
    def __init__(self, field_name, max_digits, decimal_places, is_required=False, validator_list=[]):
        self.max_digits, self.decimal_places = max_digits, decimal_places
        validator_list = [self.isValidFloat] + validator_list
        TextField.__init__(self, field_name, max_digits+1, max_digits+1, is_required, validator_list)

    def isValidFloat(self, field_data, all_data):
        v = validators.IsValidFloat(self.max_digits, self.decimal_places)
        try:
            v(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

    def html2python(data):
        if data == '' or data is None:
            return None
        return float(data)
    html2python = staticmethod(html2python)

###################
# DATES ET HEURES #
###################

class DatetimeField(TextField):
    """Une sous-classe de FormField qui convertit automatiquement ses données en un objet
    datetime.datetime. Les données passés à ce champ doivent être sous la forme AAAA-MM-JJ HH:MM:SS."""
    def __init__(self, field_name, length=30, maxlength=None, is_required=False, validator_list=[]):
        self.field_name = field_name
        self.length, self.maxlength = length, maxlength
        self.is_required = is_required
        self.validator_list = [validators.isValidANSIDatetime] + validator_list

    def html2python(data):
        "Convertit le champ en un objet datetime.datetime"
        import datetime
        date, time = data.split()
        y, m, d = date.split('-')
        timebits = time.split(':')
        h, mn = timebits[:2]
        if len(timebits) > 2:
            s = int(timebits[2])
        else:
            s = 0
        return datetime.datetime(int(y), int(m), int(d), int(h), int(mn), s)
    html2python = staticmethod(html2python)

class DateField(TextField):
    """Une sous-classe de FormField qui convertit automatiquement ses données en un objet
    datetime.date. Les données passés à ce champ doivent être sous la forme AAAA-MM-JJ."""
    def __init__(self, field_name, is_required=False, validator_list=[]):
        validator_list = [self.isValidDate] + validator_list
        TextField.__init__(self, field_name, length=10, maxlength=10,
            is_required=is_required, validator_list=validator_list)

    def isValidDate(self, field_data, all_data):
        try:
            validators.isValidANSIDate(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

    def html2python(data):
        "Convertit le champ en un objet datetime.date"
        import time, datetime
        try:
            time_tuple = time.strptime(data, '%Y-%m-%d')
            return datetime.date(*time_tuple[0:3])
        except (ValueError, TypeError):
            return None
    html2python = staticmethod(html2python)

class TimeField(TextField):
    """Une sous-classe de FormField qui convertit automatiquement ses données en un objet
    datetime.time. Les données passés à ce champ doivent être sous la forme HH:MM:SS."""
    def __init__(self, field_name, is_required=False, validator_list=[]):
        validator_list = [self.isValidTime] + validator_list
        TextField.__init__(self, field_name, length=8, maxlength=8,
            is_required=is_required, validator_list=validator_list)

    def isValidTime(self, field_data, all_data):
        try:
            validators.isValidANSITime(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

    def html2python(data):
        "Convertit le champ en un objet datetime.time"
        import time, datetime
        try:
            try:
                time_tuple = time.strptime(data, '%H:%M:%S')
            except ValueError: # les secondes n'ont pas été spécifiées
                time_tuple = time.strptime(data, '%H:%M')
            return datetime.time(*time_tuple[3:6])
        except ValueError:
            return None
    html2python = staticmethod(html2python)

##########################
# CHAMPS LIÉS À INTERNET #
##########################

class EmailField(TextField):
    "Une sous-classe pratique de FormField qui vérifie si ses données constituent une adresse e-mail valide."
    def __init__(self, field_name, length=50, is_required=False, validator_list=[]):
        validator_list = [self.isValidEmail] + validator_list
        TextField.__init__(self, field_name, length, maxlength=75,
            is_required=is_required, validator_list=validator_list)

    def isValidEmail(self, field_data, all_data):
        try:
            validators.isValidEmail(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

class URLField(TextField):
    "Une sous-classe pratique de FormField qui vérifie si ses données constituent une URL valide."
    def __init__(self, field_name, length=50, is_required=False, validator_list=[]):
        validator_list = [self.isValidURL] + validator_list
        TextField.__init__(self, field_name, length=length, maxlength=200,
            is_required=is_required, validator_list=validator_list)

    def isValidURL(self, field_data, all_data):
        try:
            validators.isValidURL(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

class IPAddressField(TextField):
    def html2python(data):
        return data or None
    html2python = staticmethod(html2python)

##########
# DIVERS #
##########

class PhoneNumberField(TextField):
    "Une sous-classe pratique de FormField qui vérifie si ses données constituent un numéro de téléphone américain valide (par ex : '630-555-1234')."
    def __init__(self, field_name, is_required=False, validator_list=[]):
        validator_list = [self.isValidPhone] + validator_list
        TextField.__init__(self, field_name, length=12, maxlength=12,
            is_required=is_required, validator_list=validator_list)

    def isValidPhone(self, field_data, all_data):
        try:
            validators.isValidPhone(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

class USStateField(TextField):
    "Une sous-classe pratique de FormField qui vérifie si ses données constituent un état des États-Unis valide (par ex : 'IL')."
    def __init__(self, field_name, is_required=False, validator_list=[]):
        validator_list = [self.isValidUSState] + validator_list
        TextField.__init__(self, field_name, length=2, maxlength=2,
            is_required=is_required, validator_list=validator_list)

    def isValidUSState(self, field_data, all_data):
        try:
            validators.isValidUSState(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

    def html2python(data):
        return data.upper() # Le stockage devrait toujours se faire en majuscules
    html2python = staticmethod(html2python)

class CommaSeparatedIntegerField(TextField):
    "Une sous-classe pratique de FormField qui vérifie si ses données constituent une liste d'entiers (séparés par des virgules) valide."
    def __init__(self, field_name, maxlength=None, is_required=False, validator_list=[]):
        validator_list = [self.isCommaSeparatedIntegerList] + validator_list
        TextField.__init__(self, field_name, length=20, maxlength=maxlength,
            is_required=is_required, validator_list=validator_list)

    def isCommaSeparatedIntegerList(self, field_data, all_data):
        try:
            validators.isCommaSeparatedIntegerList(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages

class XMLLargeTextField(LargeTextField):
    """
    Une sous-classe de LargeTextField associée à un validateur XML. L'argument
    schema_path doit être le chemin d'accès complet d'un schéma Relax NG qui
    servira à valider le code XML passé au champ.
    """
    def __init__(self, field_name, schema_path, **kwargs):
        self.schema_path = schema_path
        kwargs.setdefault('validator_list', []).insert(0, self.isValidXML)
        LargeTextField.__init__(self, field_name, **kwargs)

    def isValidXML(self, field_data, all_data):
        v = validators.RelaxNGCompact(self.schema_path)
        try:
            v(field_data, all_data)
        except validators.ValidationError, e:
            raise validators.CriticalValidationError, e.messages
